// License Notification
//======================================================
// Copyright (c) 2010 Channel Intelligence Inc.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// ======================================================




using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.IO;
using CIP4.Common;
using CIP4.Simple;

namespace CIP4.DataProvider.XmlFile
{
    public class DataProvider : Simple.DataProvider, IDataProvider
    {
        IDataProvider Cache { get; set; }

        public delegate void SaveDocumentDelegate(XDocument document);

        public SaveDocumentDelegate SaveDocument;

        public bool Dirty { get; private set; }

        public string XmlFileName { get; set; }

        static readonly Dictionary<string, XDocument> m_XmlDocuments = new Dictionary<string,XDocument>();

        public string ConnectionString { get; private set; }

        public static Dictionary<string, XDocument> XmlDocuments
        {
            get
            {
                return m_XmlDocuments;
            }
        }

        public XDocument XmlDocument
        {
            get
            {
                if (XmlDocuments.ContainsKey(XmlFileName))
                    return XmlDocuments[XmlFileName];
                return null;
            }
            set
            {
                if (XmlDocuments.ContainsKey(XmlFileName))
                    XmlDocuments[XmlFileName] = value;
                else
                    XmlDocuments.Add(XmlFileName, value);
            }
        }

        public DateTime LastFileModifyTime = DateTime.Now;

        #region IDataProvider Members

        public DataProvider (CIP4ConnectionAttribute DataProviderConnection, IDataProvider cachingProvider, LoggerDelegate logger)
            : base (DataProviderConnection)
        {
            Cache = cachingProvider;
            if (!(DataProviderConnection is CIP4XmlFileConnectionAttribute))
                LoggerWrapper(MajorEnum.Error, MinorEnum.UnexpectedConnectionType, 
                    "{0} is not an acceptable connection type", new object[] { DataProviderConnection.ToString() }, logger);
            else
            {
                LoggerWrapper(MajorEnum.Trace, MinorEnum.InitializingProvider, 
                    "Initalizing Provider: {0}", new object[] { DataProviderConnection }, logger);
                XmlFileName = ((CIP4XmlFileConnectionAttribute)DataProviderConnection).XmlFileName;
                ConnectionString = DataProviderConnection.ToString();
                SaveDocument = delegate(XDocument document)
                {
                    using (StreamWriter wrtr = new StreamWriter(XmlFileName))
                    {
                        document.Save(wrtr);
                        wrtr.Close();
                    }
                };
            }
        }

        public void DeleteCollection(CollectionType collection, LoggerDelegate logger)
        {
            using (new CollectionLockType(collection.Name))
            {
                if (!LoggerWrapper(MajorEnum.Trace, MinorEnum.ActionStart,
                    "Started {0}", new object[] { MinorEnum.DeleteCollection }, logger))
                    return;

                lock (LoadFile())
                {
                    XElement collectionElement = GetCollectionElement(collection);
                    collectionElement.Remove();
                    LoggerWrapper(MajorEnum.Trace, MinorEnum.PurgeItems,
                        "Deleted {0} items", new object[] { collectionElement.Elements().Count() }, logger);
                    SaveDocument(XmlDocument);
                    LoggerWrapper(MajorEnum.Trace, MinorEnum.DeleteCollection,
                        "All members of collection {0} deleted", new object[] { collection.Name }, logger);
                }

                LoggerWrapper(MajorEnum.Information, MinorEnum.DeleteCollection,
                    "Collection deleted {0}", new object[] { collection }, logger);
            }
        }

        public override IEnumerable<object> GetItems(CollectionType collection, IBooleanValue selectionExpression, CursorType cursor)
        {
            using (new CollectionLockType(collection.Name))
            {
                List<object> items = new List<object>();
                if (!LoggerWrapper(MajorEnum.Trace, MinorEnum.ActionStart,
                    "Started {0}", new object[] { MinorEnum.GetItems }, cursor.Logger))
                    return items;
                try
                {
                    lock (LoadFile())
                    {
                        ISerializerType serializer = cursor.GetSerializer(SerializerKindEnum.CIP4Xml);

                        string lookForExternalId = GetExternalIdValueFromSelectionExpression(selectionExpression, serializer);

                        if (Cache != null && !string.IsNullOrEmpty(lookForExternalId))
                        {
                            foreach (object item in Cache.GetItems(collection, selectionExpression, cursor))
                            {
                                items.Add(item);
                                return items;
                            }
                        }

                        if (cursor.FailOnLinearSearch)
                            if (!LoggerWrapper(MajorEnum.Error, MinorEnum.FailOnLinearSearch,
                                "Failed on linear search accessing {0}", new object[] { collection }, cursor.Logger))
                                return items;

                        XElement collectionElement = GetCollectionElement(collection);

                        XElement nextElement = collectionElement.Element(XName.Get("Item", Globals.ciiNS));
                        int i = 0;
                        bool startProcessing = string.IsNullOrEmpty(cursor.Marker);
                        while (nextElement != null)
                        {
                            if (startProcessing)
                            {
                                object item = serializer.GetObjectForData(nextElement);
                                if (item != null)
                                {
                                    cursor.Marker = serializer.GetExternalID(item).ToString();
                                    if (CompareProperties(selectionExpression, item, serializer))
                                    {
                                        items.Add(item);
                                        i += 1;
                                    }
                                }
                            }
                            startProcessing = startProcessing || ExternalIDFromElement(nextElement) == cursor.Marker;
                            // using the current nextElement will not work in the presence of a delete. The 
                            // nextNextElement is not returned and so cannot be deleted and will be present
                            // on the call after the break allowing us to pick up where we left off.
                            XElement nextNextElement = nextElement.NextSiblingElement();
                            if (i >= cursor.PageSize)
                            {
                                break;
                            }
                            nextElement = nextNextElement;
                        }
                        return items;
                    }
                }
                finally
                {
                    LoggerWrapper(MajorEnum.Information, MinorEnum.GetItems,
                        "Get {0} items from {1}", new object[] { items.Count, collection }, cursor.Logger);
                }
            }
        }

        public override void PurgeItems(IEnumerable<ExternalIDType> externalIDs, CollectionType collection, GetSerializerDelegate getSerializer)
        {
            using (new CollectionLockType(collection.Name))
            {
                ISerializerType serializer = getSerializer(SerializerKindEnum.CIP4Xml);
                if (!LoggerWrapper(MajorEnum.Trace, MinorEnum.ActionStart,
                    "Started {0}", new object[] { MinorEnum.PurgeItems }, serializer.Logger))
                    return;

                lock (LoadFile())
                {
                    XElement collectionElement = GetCollectionElement(collection);
                    HashSet<string> dictionary =
                        GetHashSet(externalIDs);
                    int deletedCount = 0;
                    List<XElement> itemElements = new List<XElement>();
                    foreach (XElement itemElement in collectionElement.Elements().Where(e => dictionary.Contains(ExternalIDFromElement(e))))
                        itemElements.Add(itemElement);
                    foreach (XElement itemElement in itemElements)
                    {
                        itemElement.Remove();
                        LoggerWrapper(MajorEnum.Trace, MinorEnum.ItemDeleted,
                            "deleted item {0}", new object[] { itemElement.Element(XName.Get("ExternalID", Globals.ciiNS)).Value }, serializer.Logger);
                        deletedCount += 1;
                    }
                    if (deletedCount == 0)
                        LoggerWrapper(MajorEnum.Warning, MinorEnum.NothingDeleted, "Nothing deleted from {0}", new object[] { collection.Name }, serializer.Logger);
                    else
                        Dirty = true;
                    LoggerWrapper(MajorEnum.Trace, MinorEnum.ItemsActuallyPurged,
                        "{0} items actually deleted from {1}", new object[] { deletedCount, collection.Name }, serializer.Logger);
                    LoggerWrapper(MajorEnum.Information, MinorEnum.PurgeItems,
                        "{0} items deleted from {1}", new object[] { dictionary.Count, collection.Name }, serializer.Logger);
                }
                if (Cache != null)
                    Cache.PurgeItems(externalIDs, collection, getSerializer);

                if (Dirty && SaveDocument != null)
                    SaveDocument(XmlDocument);
                Dirty = false;
            }
        }

        public void SaveItems(IEnumerable<object> items, CollectionType collection, GetSerializerDelegate getSerializer)
        {
            using (new CollectionLockType(collection.Name))
            {
                ISerializerType serializer = getSerializer(SerializerKindEnum.CIP4Xml);
                if (!LoggerWrapper(MajorEnum.Trace, MinorEnum.ActionStart,
                    "Started {0}", new object[] { MinorEnum.SaveItems }, serializer.Logger))
                    return;

                // In the pious hope that we will actually get to the GetDictionary call and that it will not return null. In either 
                // of these eventualities we are heading for a null reference exception in the last statement in this method. 
                // Declaring these here so that the cache call does not have to be done inside the lock(LoadFile()) and the final 
                // Logger call can be done after the cache call has successfully completed.

                Dictionary<string, object> dictionary;

                List<XElement> itemElements = new List<XElement>();

                lock (LoadFile())
                {
                    XElement collectionElement = GetCollectionElement(collection);
                    dictionary = GetDictionary(
                        items,
                        serializer,
                        delegate(object item)
                        {
                            if (!SetHeaderProperties(item, collection, serializer))
                                return false;
                            return true;
                        });
                    var q =
                        from r in collectionElement.Elements(XName.Get("Item", Globals.ciiNS))
                        where CompareIdProperties(r, dictionary)
                        select r;
                    foreach (XElement itemElement in q)
                        itemElements.Add(itemElement);
                    foreach (XElement itemElement in itemElements)
                    {
                        object item = dictionary[ExternalIDFromElement(itemElement)];

                        if (serializer.CheckVersion)
                        {
                            long oldVersionNumber = serializer.GetVersionNumber(serializer.GetObjectForData(itemElement));
                            long newVersionNumber = serializer.GetVersionNumber(item);
                            if (oldVersionNumber > newVersionNumber)
                                if (serializer.Logger(MajorEnum.Error, MinorEnum.StaleItemOnSave, 
                                    "The Item {0} has been updated by another user", 
                                    new object[] { serializer.GetExternalID(item).ToString() }) == LoggerResponseEnum.giveUp)
                                    return;
                        }

                        itemElement.ReplaceWith(XElement.Parse(serializer.GetDataForObject(item).ToString()));
                        LoggerWrapper(MajorEnum.Trace, MinorEnum.ItemsUpdated, "Items {0} updated", new object[] { serializer.GetExternalID(item) }, serializer.Logger);
                        dictionary.Remove(serializer.GetExternalID(item).ToString());
                    }

                    // Anything items left in dictionary did not exist before in the collection, so add them
                    foreach (object item in dictionary.Values)
                    {
                        collectionElement.Add(XElement.Parse(serializer.GetDataForObject(item).ToString()));
                        LoggerWrapper(MajorEnum.Trace, MinorEnum.ItemsAdded, "Items {0} added", new object[] { serializer.GetExternalID(item) }, serializer.Logger);
                    }
                    if ((itemElements.Count + dictionary.Values.Count) > 0)
                        Dirty = true;

                }

                if (Cache != null)
                    Cache.SaveItems(items, collection, getSerializer);

                LoggerWrapper(MajorEnum.Information, MinorEnum.SaveItems,
                    "{0} items saved in {1}", new object[] { items.Count(), collection.Name }, serializer.Logger);
                // Note the use of "items.Count()" here - what we want to say to the caller is 
                // "All the stuff you gave me has been saved - actually if you gave me two items 
                // with the same ExternalID, I only saved the last one - but you can't tell  
                // from a GetItems call that I didn't save the first one and then save the last one
                // so I can claim to have saved both even though I didn't. If you really want to
                // know what's going on you'd have to look at the Items updated and Items added trace messages"

                if (Dirty && SaveDocument != null)
                    SaveDocument(XmlDocument);
                Dirty = false;
            }
        }

        public void Terminate(LoggerDelegate logger)
        {
        }

        #endregion

        #region Helper Functions

        private XDocument LoadFile()
        {
            if (!XmlDocuments.ContainsKey(XmlFileName) && !File.Exists(XmlFileName))
            {
                XmlDocument = new XDocument();
                XmlDocument.Add(new XElement(XName.Get("do", Globals.cicNS)));
            }
            else if (File.Exists(XmlFileName))
            {
                DateTime nextModifyTime = new FileInfo(XmlFileName).LastWriteTime;
                if (XmlDocument == null ||
                    nextModifyTime != LastFileModifyTime)
                {
                    StreamReader rdr = new StreamReader(XmlFileName);
                    XmlDocument = XDocument.Load(rdr);
                    rdr.Close();
                    LastFileModifyTime = nextModifyTime;
                }
            }
            return XmlDocument;
        }

        private XElement GetCollectionElement(CollectionType collection)
        {
            var query =
                    from c in XmlDocument.Root.Elements(XName.Get("publish", Globals.cicNS))
                    where c.Attribute(XName.Get("destination", Globals.cicNS)).Value == collection.Name
                    select c;

            foreach (XElement result in query)
                return result;
            XElement newElement = new XElement(XName.Get("publish", Globals.cicNS));
            newElement.Add(new XAttribute(XName.Get("destination", Globals.cicNS), collection.Name));
            XmlDocument.Root.Add(newElement);
            return newElement;
        }

        private static bool CompareIdProperties(XElement item, Dictionary<string, object> dictionary)
        {
            string exId = ExternalIDFromElement(item);
            if (!dictionary.ContainsKey(exId))
                return false;
            return true;
        }

        private static string PropertyFromElement(XElement item, string propertyName)
        {
            XElement pElement = item.Element(XName.Get(propertyName, Globals.ciiNS));
            if (pElement != null)
                return pElement.Value;
            return string.Empty;
        }

        private static string ExternalIDFromElement(XElement item)
        {
            return PropertyFromElement(item, "ExternalID");
        }

        private delegate bool ValidateItemDelegate(object item);

        private Dictionary<string, object> GetDictionary(IEnumerable<object> items, ISerializerType serializer, ValidateItemDelegate validateItem)
        {
            Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.CurrentCultureIgnoreCase);
            foreach (object itemObject in items)
            {
                if (!validateItem(itemObject))
                    return new Dictionary<string, object>();
                string externalID = serializer.GetExternalID(itemObject).ToString();
                if (dictionary.ContainsKey(externalID))
                {
                    LoggerWrapper(MajorEnum.Warning, MinorEnum.DuplicateParameterItems,
                        "There are two or more items with the same externalID ({0}) in the list of items passed in - only the last will be used", new object[] { externalID }, serializer.Logger);
                    dictionary[externalID] = itemObject;
                }
                else
                {
                    dictionary.Add(externalID, itemObject);
                }
            }
            return dictionary;
        }

        private static HashSet<string> GetHashSet(IEnumerable<ExternalIDType> externalIDs)
        {
            HashSet<string> dictionary = new HashSet<string>(StringComparer.CurrentCultureIgnoreCase);
            foreach (ExternalIDType externalID in externalIDs)
            {
                if (!dictionary.Contains(externalID.ToString()))
                {
                    dictionary.Add(externalID.ToString());
                }
            }
            return dictionary;
        }

        #endregion
    }
}
